﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using Myotragus.Data.Util;

namespace Myotragus.Data.Emit
{
	public class DynamicType : IDynamicType
	{
		protected const MethodAttributes PropertyAccessorMethodAttributes =
			MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName |
			MethodAttributes.Virtual | MethodAttributes.NewSlot;

		private readonly TypeBuilder _builder;
		private readonly INameGenerator _nameGenerator;

		public DynamicType(INameGenerator nameGenerator, ModuleBuilder module, string typeName, 
			TypeAttributes attributes, Type baseType, IEnumerable<Type> interfaces)
		{
			var interfacesCopy = interfaces.ToArray();
			_builder = module.DefineType(typeName, attributes, baseType, interfacesCopy);
			_nameGenerator = nameGenerator;

			Attributes = attributes;
			BaseType = baseType;
			TypeName = typeName;
			_interfaces.AddRange(interfacesCopy);
		}

		public void DefineAutomaticProperty(string propertyName, Type propertyType)
		{
			var builder = CreateProperty(_builder, propertyName, propertyType);
			var field = CreateBackingField(_builder, propertyName, propertyType);
			var setter = CreateSetter(_builder, field, propertyName, propertyType);
			var getter = CreateGetter(_builder, field, propertyName, propertyType);

			builder.SetSetMethod(setter);
			builder.SetGetMethod(getter);
		}

		public void DefineDefaultConstructor(MethodAttributes attributes)
		{
			_builder.DefineDefaultConstructor(attributes);
		}

		public void AddInterfaceImplementation(Type interfaceType)
		{
			_builder.AddInterfaceImplementation(interfaceType);
			_interfaces.Add(interfaceType);
		}

		public void SetCustomAttribute(ConstructorInfo attributeCtor, params object[] attributeParams)
		{
			SetCustomAttribute(attributeCtor, (IEnumerable<object>)attributeParams);
		}

		public void SetCustomAttribute(ConstructorInfo attributeCtor, IEnumerable<object> attributeParams)
		{
			_builder.SetCustomAttribute(
				new CustomAttributeBuilder(attributeCtor, attributeParams.ToArray()));
		}

		public Type CreateType()
		{
			try
			{
				return _builder.CreateType();
			}
			catch (Exception ex)
			{
				throw ExecutionHelper.CreateException<TypeGenerationException>(ex,
					"Could not create instance");
			}
		}

		protected PropertyBuilder CreateProperty(TypeBuilder type, string propertyName, Type propertyType)
		{
			return type.DefineProperty(propertyName, PropertyAttributes.None, propertyType,
				ReflectionHelper.EmptyTypes);
		}

		protected MethodBuilder CreateGetter(TypeBuilder type, FieldInfo field, string propertyName,
			Type propertyType)
		{
			var mth = type.DefineMethod("get_" + propertyName, PropertyAccessorMethodAttributes,
				CallingConventions.HasThis, propertyType, ReflectionHelper.EmptyTypes);

			var gen = mth.GetILGenerator();

			gen.DeclareLocal(propertyType);
			var lbl = gen.DefineLabel();

			gen.Emit(OpCodes.Ldarg_0);
			gen.Emit(OpCodes.Ldfld, field);
			gen.Emit(OpCodes.Stloc_0);
			gen.Emit(OpCodes.Br_S, lbl);
			gen.MarkLabel(lbl); gen.Emit(OpCodes.Ldloc_0);
			gen.Emit(OpCodes.Ret);

			return mth;
		}

		protected MethodBuilder CreateSetter(TypeBuilder type, FieldInfo field, string propertyName, Type propertyType)
		{
			var mth = type.DefineMethod("set_" + propertyName, PropertyAccessorMethodAttributes,
				CallingConventions.HasThis, ReflectionHelper.TypeOfVoid, new[] { propertyType });

			var gen = mth.GetILGenerator();

			gen.Emit(OpCodes.Ldarg_0);
			gen.Emit(OpCodes.Ldarg_1);
			gen.Emit(OpCodes.Stfld, field);
			gen.Emit(OpCodes.Ret);

			return mth;
		}

		protected FieldBuilder CreateBackingField(TypeBuilder type, string propertyName, Type propertyType)
		{
			return type.DefineField(_nameGenerator.CreateFieldName(propertyName), propertyType, FieldAttributes.Private);
		}

		#region IDynamicType Members

		private readonly List<Type> _interfaces = new List<Type>();

		public IEnumerable<Type> Interfaces
		{
			get { return _interfaces; }
		}

		public Type BaseType { get; private set; }

		public string TypeName { get; private set; }

		public TypeAttributes Attributes { get; private set; }

		#endregion IDynamicType Members
	}
}